﻿#region copyright
//Copyright (c) 2011, Check In Solutions LLC
//All rights reserved.

//Redistribution and use in source and binary forms, with or without
//modification, are permitted provided that the following conditions are met:
//    * Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//    * Neither the name of the Check In Solutions LLC nor the
//      names of its contributors may be used to endorse or promote products
//      derived from this software without specific prior written permission.

//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//DISCLAIMED. IN NO EVENT SHALL CHECK IN SOLUTIONS LLC BE LIABLE FOR ANY
//DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml.Serialization;
using System.Xml;
using System.IO;
using System.Security.Cryptography;
using System.IO.Compression;

namespace CheckInSolutions.Messaging
{
    /// <summary>
    /// Delgate for logging errors on byte to object conversion
    /// </summary>
    /// <param name="e">Exception e</param>
    public delegate void MessageCryptoExceptionHandler(Exception e);

    /// <summary>
    /// Used to hold an message object and payload object. The Payload can be compress/encrypted.
    /// #if STREAM_CRYPTO, then the crypto and compress streams are done at once. #else they are done in two steps.
    /// To uncompress, a lenght counter must be written at the head of the byte array. 
    /// If crpto is not 'streamed' then a 4-byte count is at head of the encryption byte array. This lowers security.
    /// </summary>
    /// <typeparam name="P">Payload for a QMessage</typeparam>
    public class EQMessage<TMessage, TPayload> : QMessage where TMessage : new()
    {
        #region Constructors

        public EQMessage()
        {
            this.MessageSerializer = new Serializer<TMessage>();
            this.SetUpEventHanders();
        }

        public EQMessage(QMessage qMessage)
        {
            this.MessageSerializer = new Serializer<TMessage>();
            this.SetUpEventHanders();
            this.MessageXML = qMessage.MessageXML;
            this.PayloadBytes = qMessage.PayloadBytes;
        }

        public EQMessage(TMessage message, TPayload payload)
        {
            this.MessageSerializer = new Serializer<TMessage>();
            this.SetUpEventHanders();
            this.Message = message;
            this.Payload = payload;
        }
        #endregion

        #region Enums
        public enum PayloadStatusType
        {
            Native,
            Compressed,
            Encrypted
        }
        #endregion

        #region Properties
        protected MessageCryptoExceptionHandler m_MessageCryptoExceptionHandler = null;
        protected PayloadStatusType m_PayloadStatusType = PayloadStatusType.Native;
        protected MessageCrypto m_MessageCrypto;
        protected Serializer<TMessage> m_MessageSerializer;
        protected TMessage m_Message;
        protected TPayload m_Payload;

        /// <summary>
        /// Internal use, serializer for messages in and out.
        /// </summary>
        protected Serializer<TMessage> MessageSerializer
        {
            get { return this.m_MessageSerializer; }
            set { this.m_MessageSerializer = value; }
        }

        [XmlIgnore]
        public PayloadStatusType PayloadStatus
        {
            get { return this.m_PayloadStatusType; }
            set
            {
                if (this.m_PayloadStatusType != value)
                {
                    if (this.m_PayloadStatusType == PayloadStatusType.Compressed)
                        throw new ArgumentException("Compression cannot be turned off through PayloadStatus. Reset Encryption object first.");
                    else
                    {
                        this.m_PayloadStatusType = value;
                        this.SetPayloadBytes();
                    }
                }
            }
        }

        /// <summary>
        /// Set a message handler for system exception logging.
        /// </summary>
        [XmlIgnore]
        public MessageCryptoExceptionHandler setExceptionHandler
        {
            set { this.m_MessageCryptoExceptionHandler = value; }
        }

        /// <summary>
        /// Interface to an encryption scheme.
        /// </summary>
        /// <exception cref="System.NullReferenceException">Indicates a need to use it prior to the value being set.</exception>
        [XmlIgnore]
        public MessageCrypto MessageCrypto
        {
            protected get
            {
                if (this.m_MessageCrypto is MessageCrypto)
                    return this.m_MessageCrypto;
                else
                    throw new NullReferenceException(String.Format("MessageCrypto was not set before needed in EQMessage<{0}, {1}>", typeof(TMessage).ToString(), typeof(TPayload).ToString()));
            }
            set
            {
                this.m_MessageCrypto = value;
                if (this.m_MessageCrypto is MessageCrypto)
                {
                    this.m_PayloadStatusType = PayloadStatusType.Encrypted;
                    this.CryptoName = this.m_MessageCrypto.Name;
                }
                else
                {
                    this.m_PayloadStatusType = PayloadStatusType.Compressed;
                    this.CryptoName = String.Empty;
                }
                this.SetPayloadBytes();
            }
        }

        /// <summary>
        /// Message object
        /// </summary>
        [XmlIgnore]
        public TMessage Message
        {
            get { return this.m_Message; }
            set
            {
                this.m_Message = value;
                this.m_MessageXml = this.MessageSerializer.serializetoString(this.m_Message);
            }
        }

        ///// <summary>
        ///// used in object serialization by handling transports.
        ///// </summary>
        //public string MessageXML
        //{
        //    get { return this.MessageSerializer.serializetoString(this.Message); }
        //    set { this.m_Message = this.MessageSerializer.deserializeFromString(value); }
        //}

        /// <summary>
        /// return Payload object
        /// </summary>
        [XmlIgnore]
        public TPayload Payload
        {
            get { return this.m_Payload; }
            set
            {
                this.m_Payload = value;
                this.SetPayloadBytes();
            }
        }

        [XmlIgnore]
        public QMessage TransportMessage
        {
            get { return (QMessage)base.Clone(); }
        }

        #endregion

        #region Methods
        /// <summary>
        /// Invoke a delegated message handler if set. 
        /// If not the inheriting class can rewrite the logger directly.
        /// </summary>
        /// <param name="e">Some Exception as thrown</param>
        protected virtual void LogError(Exception e)
        {
            if (this.m_MessageCryptoExceptionHandler is MessageCryptoExceptionHandler)
            {
                this.m_MessageCryptoExceptionHandler(e);
            }
        }

        protected void ByteToStream(Stream s, byte[] b)
        {
            BinaryWriter bw = new BinaryWriter(s);
            bw.Write(b.Length);
            bw.Write(b, 0, b.Length);
            //Close, DO NOT FLUSH cause bytes will go missing...
            bw.Close();
        }

        protected byte[] StreamToByte(Stream s)
        {
            BinaryReader br = new BinaryReader(s);
            int count = br.ReadInt32();
            return br.ReadBytes(count);
        }


        protected void SetPayloadBytes()
        {
            if (this.Payload is TPayload)
            {
                byte[] retVal = new byte[0];
                using (MemoryStream objectStream = new MemoryStream())
                {
                    try
                    {
#if STREAM_CRYPTO
                        BinaryFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(objectStream, this.Payload);
                        retVal = objectStream.ToArray();

                        if (this.EncryptedPayload)
                        {
                            using (MemoryStream outputStream = new MemoryStream())
                            {
                                CryptoStream cryptoStream = new CryptoStream(outputStream, this.EncryptionTransform, CryptoStreamMode.Write);
                                GZipStream zipStream = new GZipStream(cryptoStream, CompressionMode.Compress);
                                this.ByteToStream(zipStream, retVal);
                                retVal = outputStream.ToArray();
                            }
                        }
                        else if (this.m_CompressedPayload)
                        {
                            using (MemoryStream outputStream = new MemoryStream())
                            {
                                GZipStream zipStream = new GZipStream(outputStream, CompressionMode.Compress);
                                this.ByteToStream(zipStream, retVal);
                                retVal = outputStream.ToArray();
                            }
                        }


#else

                        BinaryFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(objectStream, this.Payload);
                        retVal = objectStream.ToArray();

                        switch (this.PayloadStatus)
                        {
                            case EQMessage<TMessage, TPayload>.PayloadStatusType.Native:
                                break;
                            case EQMessage<TMessage, TPayload>.PayloadStatusType.Compressed:
                                using (MemoryStream outputStream = new MemoryStream())
                                {
                                    GZipStream zipStream = new GZipStream(outputStream, CompressionMode.Compress);
                                    this.ByteToStream(zipStream, retVal);
                                    retVal = outputStream.ToArray();
                                } break;
                            case EQMessage<TMessage, TPayload>.PayloadStatusType.Encrypted:
                                using (MemoryStream outputStream = new MemoryStream())
                                {
                                    GZipStream zipStream = new GZipStream(outputStream, CompressionMode.Compress);
                                    this.ByteToStream(zipStream, retVal);
                                    retVal = outputStream.ToArray();
                                }
                                using (MemoryStream outputStream = new MemoryStream())
                                {
                                    CryptoStream cryptoStream = new CryptoStream(outputStream, this.MessageCrypto.Encrypter, CryptoStreamMode.Write);
                                    this.ByteToStream(cryptoStream, retVal);
                                    retVal = outputStream.ToArray();
                                }
                                break;
                            default:
                                break;
                        }
#endif
                    }
                    catch (System.ArgumentNullException e)
                    {
                        this.LogError(e);
                    }
                    catch (System.Runtime.Serialization.SerializationException e)
                    {
                        this.LogError(e);
                    }
                    catch (System.Security.SecurityException e)
                    {
                        this.LogError(e);
                    }
                }
                this.m_PayloadBytes = retVal;  // note we avoid the property because we do NOT want to trigger an event.
            }
        }

        protected void SetUpEventHanders()
        {
            this.ChangedMessageXML += new ChangedMessageXMLHandler(SetMessageOnXMLChanged);
            this.ChangedPayloadBytes += new ChangedPayloadBytesHandler(this.SetPayloadOnBytesChanged);
        }
        #endregion

        #region Events
        #endregion

        #region EventHandlers
        public void SetPayloadOnBytesChanged()
        {
            byte[] b = this.m_PayloadBytes;
            BinaryFormatter formatter = new BinaryFormatter();
            try
            {
#if STREAM_CRYPTO
                if (this.CanEncrypt)
                {
                    using (MemoryStream objectStream = new MemoryStream(b))
                    {
                        CryptoStream cryptoStream = new CryptoStream(objectStream, this.DecryptionTransform, CryptoStreamMode.Read);
                        GZipStream zipStream = new GZipStream(cryptoStream, CompressionMode.Decompress);
                        b = this.StreamToByte(zipStream);
                    }
                }
                else if (this.ShouldCompressPayload)
                {
                    using (MemoryStream objectStream = new MemoryStream(b))
                    {
                        GZipStream zipStream = new GZipStream(objectStream, CompressionMode.Decompress);
                        b = this.StreamToByte(zipStream);
                    }
                }
                using (MemoryStream objectStream = new MemoryStream(b))
                {
                    this.Payload = (TPayload)formatter.Deserialize(objectStream);
                }
#else   
                switch (this.PayloadStatus)
                {
                    case EQMessage<TMessage, TPayload>.PayloadStatusType.Native:
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            this.m_Payload = (TPayload)formatter.Deserialize(objectStream);
                        }
                        break;
                    case EQMessage<TMessage, TPayload>.PayloadStatusType.Compressed:
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            GZipStream zipStream = new GZipStream(objectStream, CompressionMode.Decompress);
                            b = this.StreamToByte(zipStream);
                        }
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            this.m_Payload = (TPayload)formatter.Deserialize(objectStream);
                        }
                        break;
                    case EQMessage<TMessage, TPayload>.PayloadStatusType.Encrypted:
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            CryptoStream cryptoStream = new CryptoStream(objectStream, this.MessageCrypto.Decrypter, CryptoStreamMode.Read);
                            b = this.StreamToByte(cryptoStream);
                        }
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            GZipStream zipStream = new GZipStream(objectStream, CompressionMode.Decompress);
                            b = this.StreamToByte(zipStream);
                        }
                        using (MemoryStream objectStream = new MemoryStream(b))
                        {
                            this.m_Payload = (TPayload)formatter.Deserialize(objectStream);
                        }
                        break;
                    default:
                        break;
                }
#endif
            }
            catch (System.ArgumentNullException e)
            {
                this.LogError(e);
            }
            catch (System.Runtime.Serialization.SerializationException e)
            {
                this.LogError(e);
            }
            catch (System.Security.SecurityException e)
            {
                this.LogError(e);
            }
            catch (System.InvalidCastException e)
            {
                this.LogError(e);
            }
        }


        public void SetMessageOnXMLChanged()
        {
            this.m_Message = this.MessageSerializer.deserializeFromString(this.m_MessageXml);
        }
        #endregion

        #region Interface Implementations
        #endregion


    }
}

//        /// <summary>
//        /// used in object serialization by handling transports.
//        /// </summary>
//        protected void SetPayloadBytes()
//        {
//            byte[] retVal = new byte[0];
//            using (MemoryStream objectStream = new MemoryStream())
//            {
//                try
//                {
//#if STREAM_CRYPTO
//                    BinaryFormatter formatter = new BinaryFormatter();
//                    formatter.Serialize(objectStream, this.Payload);
//                    retVal = objectStream.ToArray();

//                    if (this.CanEncrypt)
//                    {
//                        using (MemoryStream outputStream = new MemoryStream())
//                        {
//                            CryptoStream cryptoStream = new CryptoStream(outputStream, this.EncryptionTransform, CryptoStreamMode.Write);
//                            GZipStream zipStream = new GZipStream(cryptoStream, CompressionMode.Compress);
//                            this.ByteToStream(zipStream, retVal);
//                            retVal = outputStream.ToArray();
//                        }
//                    }
//                    else if (this.m_ShouldCompressPayload)
//                    {
//                        using (MemoryStream outputStream = new MemoryStream())
//                        {
//                            GZipStream zipStream = new GZipStream(outputStream, CompressionMode.Compress);
//                            this.ByteToStream(zipStream, retVal);
//                            retVal = outputStream.ToArray();
//                        }
//                    }


//#else

//                        BinaryFormatter formatter = new BinaryFormatter();
//                        formatter.Serialize(objectStream, this.Payload);
//                        retVal = objectStream.ToArray();

//                        if (this.m_ShouldCompressPayload)
//                        {
//                            using (MemoryStream outputStream = new MemoryStream())
//                            {
//                                GZipStream zipStream = new GZipStream(outputStream, CompressionMode.Compress);
//                                this.ByteToStream(zipStream, retVal);
//                                retVal = outputStream.ToArray();
//                            }
//                        }

//                        if (this.CanEncrypt)
//                        {
//                            using (MemoryStream outputStream = new MemoryStream())
//                            {
//                                CryptoStream cryptoStream = new CryptoStream(outputStream, this.EncryptionTransform, CryptoStreamMode.Write);
//                                this.ByteToStream(cryptoStream, retVal);
//                                retVal = outputStream.ToArray();
//                            }
//                        }
//#endif
//                }
//                catch (System.ArgumentNullException e)
//                {
//                    this.LogError(e);
//                }
//                catch (System.Runtime.Serialization.SerializationException e)
//                {
//                    this.LogError(e);
//                }
//                catch (System.Security.SecurityException e)
//                {
//                    this.LogError(e);
//                }
//            }
//            this.m_PayloadBytes = retVal;  // note we avoid the property because we do NOT want to trigger an event.
//        }

